refactor(cli): clean up compilation config and registry cursor#1367
Conversation
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR refactors the CLI compilation dry-run config cloning to use dataclasses.replace() (avoiding field drift as CompilationConfig evolves) and tightens registry pagination parsing to only accept the spec-compliant metadata.nextCursor.
Changes:
- Swap manual
CompilationConfig(...)field mapping fordataclasses.replace(config, ...)in single-file dry-run compilation. - Enforce v0.1 MCP registry spec compliance by removing legacy
next_cursorpagination fallback.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/apm_cli/commands/compile/cli.py | Uses dataclasses.replace() to derive the intermediate dry-run config without manually mapping fields. |
| src/apm_cli/registry/client.py | Removes acceptance of legacy snake_case cursor key, reading only metadata.nextCursor. |
1d38cb9 to
31eca32
Compare
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 1 | Both changes are correct and well-scoped. dataclasses.replace() closes a silent field-default hole; next_cursor cleanup removes dead code. No architectural concerns. |
| CLI Logging Expert | 0 | 0 | 0 | Neither change touches user-facing CLI output, logging, STATUS_SYMBOLS, CommandLogger, or any console helper. No findings. |
| DevX UX Expert | 0 | 0 | 0 | Neither change touches CLI surface, command flags, error wording, help text, or install/run flows. Correctness improvement only. |
| Supply Chain Security | 0 | 0 | 0 | No supply chain concerns. stdlib dataclasses adds no dependency; cursor key alignment has no trust-boundary implications. |
| OSS Growth Hacker | 0 | 1 | 1 | Pure internal cleanup. CHANGELOG entry for cursor removal recommended. dataclasses.replace() idiom worth surfacing for contributors. |
| Test Coverage Expert | 0 | 0 | 2 | Existing CLI regression trap covers the compile path. Camelcase cursor path regression-trapped. Nit gap: no test asserts non-default field preservation through replace(). |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 3 follow-ups
- [OSS Growth Hacker] Add a one-line CHANGELOG entry under
Removedfor thenext_cursorsnake_case fallback drop -- Any external registry implementor relying on the snake_case key gets a visible upgrade path; models OSS hygiene for new contributors. Only recommended-tier finding in the entire panel. - [Test Coverage Expert] Add a unit test asserting non-default field preservation through
dataclasses.replace()-- The existing regression trap (test_codex_single_agents_no_claude_preview_issue_765_cli) does not setlocal_only,trace,debug, orexcludeto non-default values; a future regression where one of those fields begins defaulting again would be invisible to CI. Low-cost test, high-value guardrail. - [Python Architect] Add a comment in
CompilationConfig.__post_init__noting thatdataclasses.replace()retriggers it -- Pre-existing fragility: if a future caller doesreplace(config, single_agents=False, strategy='distributed'),__post_init__will not overridestrategybecausesingle_agentsis False. A comment makes the precedence contract explicit and prevents misuse.
Architecture
classDiagram
direction LR
class CompilationConfig {
<<Dataclass>>
+output_path str
+chatmode str
+resolve_links bool
+dry_run bool
+with_constitution bool
+target CompileTargetType
+strategy str
+single_agents bool
+trace bool
+local_only bool
+debug bool
+min_instructions_per_file int
+source_attribution bool
+clean_orphaned bool
+exclude list
+__post_init__()
+from_apm_yml(**overrides) CompilationConfig
}
class AgentsCompiler {
<<Service>>
+compile(config) CompilationResult
}
class CompilationResult {
<<ValueObject>>
+success bool
+content str
}
class compile_cli {
<<IOBoundary>>
+run(config)
}
class ConstitutionInjector {
<<Service>>
+inject(content, with_constitution, output_path)
}
compile_cli ..> CompilationConfig : creates via dataclasses.replace
compile_cli *-- AgentsCompiler : calls
compile_cli *-- ConstitutionInjector : calls
AgentsCompiler ..> CompilationConfig : reads
AgentsCompiler ..> CompilationResult : returns
class CompilationConfig:::touched
class compile_cli:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A([apm compile - single-file branch]) --> B[load CompilationConfig from apm.yml + CLI flags]
B --> C{strategy == single-file?}
C -- No --> D[distributed compilation path - not in scope]
C -- Yes --> E["dataclasses.replace(config, dry_run=True, strategy=single-file)\n=> intermediate_config (all 14 fields propagated)"]
E --> F["AgentsCompiler.compile(intermediate_config)\n=> intermediate_result"]
F --> G{intermediate_result.success?}
G -- No --> H[surface error to user]
G -- Yes --> I[ConstitutionInjector.inject]
I --> J{dry_run flag?}
J -- Yes --> K[print content, skip write]
J -- No --> L[write final_content to output_path]
Recommendation
Panel is unanimous at the nit level or below. The correctness fix (dataclasses.replace) and the dead-code removal (next_cursor fallback) are both safe, well-scoped, and regression-trapped. The three follow-ups are all nit or recommended tier and none require pre-merge resolution. Ship now; file the CHANGELOG entry and the field-preservation test as a fast-follow in the same sprint.
Full per-persona findings
Python Architect
- [nit]
CompilationConfig.__post_init__side-effect interacts withdataclasses.replace()atsrc/apm_cli/compilation/agents_compiler.py:91
__post_init__runs after everydataclasses.replace()call. The replace setsstrategy='single-file'explicitly, andsingle_agentsis copied from config. If the original config hadsingle_agents=True,__post_init__will overwrite strategy to'single-file'again -- harmless here. But if a future caller doesdataclasses.replace(config, single_agents=False, strategy='distributed'),__post_init__would NOT override strategy. The current PR is safe; the fragility is pre-existing.
Suggested:# NOTE: dataclasses.replace() also triggers __post_init__; ensure flag precedence logic remains idempotent.
CLI Logging Expert
No findings.
DevX UX Expert
No findings.
Supply Chain Security
No findings.
OSS Growth Hacker
- [recommended] No CHANGELOG entry for the
next_cursorsnake_case fallback removal atsrc/apm_cli/registry/client.py
If any external registry implementors exist, dropping the snake_case'next_cursor'fallback silently breaks them with no upgrade path visible in the changelog. A one-line entry underRemovedorChangedmodels good OSS hygiene and signals to contributors that even small removals are documented. - [nit]
dataclasses.replace()idiom could be highlighted in contributor docs atsrc/apm_cli/commands/compile/cli.py
The fix is a good contributor pattern -- stdlib over manual field copying. Worth a comment in the code or a CONTRIBUTING.md callout so new contributors learn this idiom and apply it elsewhere. No action required in this PR.
Auth Expert -- inactive
No auth surface touched: changed files are compile/cli.py (internal config refactor) and registry/client.py (cursor key cleanup), neither affecting token management, credential resolution, or AuthResolver.
Doc Writer -- inactive
No documentation surface touched; internal cleanup only (dataclasses.replace refactor and legacy cursor fallback removal).
Test Coverage Expert
- [nit] CLI test for
dataclasses.replace()does not assert non-default field preservation attests/unit/compilation/test_compile_target_flag.py
The existing test (test_codex_single_agents_no_claude_preview_issue_765_cli) invokesapm compile --target codex --single-agents --dry-runand asserts exit code 0, AGENTS.md in output, and absence of Claude preview text. However, the test never sets fields likelocal_only,trace,debug,exclude,min_instructions_per_file,source_attribution, orclean_orphanedto non-default values. Thedataclasses.replace()change is correct, but no test would catch a future regression where one of those fields begins defaulting again.
Suggested: Add a unit test that constructsCompilationConfigwith e.g.local_only=True, trace=True, then callsdataclasses.replace(config, dry_run=True, strategy='single-file')and asserts all non-overridden fields are unchanged.
Proof (passed):tests/unit/compilation/test_compile_target_flag.py::test_codex_single_agents_no_claude_preview_issue_765_cli-- proves: CLI compile --target codex --single-agents --dry-run routes to AGENTS.md without appending Claude preview text [devx]
assert 'AGENTS.md' in result.output and 'CLAUDE.md Preview' not in result.output - [nit] No test for removed snake_case
next_cursorfallback -- deletion is clean attests/unit/test_registry_client.py
Probe oftests/unit/test_registry_client.pyfor snake_casenext_cursorfound no test for the removed fallback path. The camelCase path is regression-trapped at line 65. Dead-code removal has no test gap to close. No action required.
Proof (passed):tests/unit/test_registry_client.py::test_list_servers_reads_nextCursor_camelCase-- proves:metadata.nextCursor(camelCase, the spec key) is read and returned correctly bylist_servers()[vendor-neutral]
self.assertEqual(next_cursor, 'spec-cursor')
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Note
🔒 Integrity filter blocked 3 items
The following items were blocked because they don't meet the GitHub integrity level.
- #1367
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - refactor(cli): clean up compilation config and registry cursor #1367
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - #1367
search_pull_requests: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by PR Review Panel for issue #1367 · ● 2.1M · ◷
* chore: cut 0.15.0 Move Unreleased -> [0.15.0] - 2026-05-27 and bump pyproject + uv.lock. Audit applied: every PR merged since v0.14.2 has exactly one changelog entry; each entry leads with the user-visible impact. Fixes during audit: - Add missing entries for #1367, #1403, #1465, #1487, #1492, #1462, #1477, #1439, #1484, and the 131679f follow-up commit. - Collapse the two #1473 lines into one. - Merge the #1476 Security/GitCache-hardening entry into its Added entry (same PR, one logical change). - Replace bogus #1243 PR ref with the actual merge PR #1308 for the persisted transport-flag config. - Relocate the #1324-delivered marketplace CLI entries (apm pack --marketplace / --marketplace-path / --json, outputs map form) out of Unreleased and into [0.14.2], where they actually shipped. They were mis-attributed to #1317 and orphaned across the 0.14.2 cut. Verified locally: ruff check + ruff format --check both clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
refactor(cli): modernize config instantiation and enforce registry spec compliance
TL;DR
This PR replaces the manual
CompilationConfigmapping in the CLI withdataclasses.replace()to prevent field-drift bugs. It also drops the legacy snake_casenext_cursorfallback in the registry client to enforce strict adherence to the v0.1 MCP registry spec.Problem (WHY)
CompilationConfigfields during dry runs. Adding a new field to configuration ran the risk of being silently dropped during dry-run processing.next_cursorkey from search metadata to support outdated in-tree mock fixtures, drifting from the exact v0.1 spec payload.Approach (WHAT)
dataclasses.replacemodule forCompilationConfig.SimpleRegistryClientto extract only the explicitly supportednextCursorkey from pagination.Implementation (HOW)
src/apm_cli/commands/compile/cli.pydataclassesand swapped the manualCompilationConfig(...)instantiation in the dry-run strategy todataclasses.replace(config, dry_run=True, strategy="single-file").src/apm_cli/registry/client.pyor metadata.get("next_cursor")assignment, natively resolving the spec-compliant"nextCursor"key from the API metadata.Diagrams
Note
Legend: The CLI compilation dry-run now strictly clones the existing configuration context using standard dataclass replacement mechanisms.
flowchart LR A[Compile Command] -->|"dry_run=True"| B(dataclasses.replace) B -->|'strategy=single-file'| C[intermediate_config] C --> D[AgentsCompiler.compile]Trade-offs
next_cursorJSON metadata fallback shape will now fail and must be updated to the v0.1 spec (nextCursor).Benefits
CompilationConfigwill automatically propagate through dry-runs without requiring synchronous CLI mapping updates.v0.1registry standards.Validation
Validation Output
Scenario Evidence:
apm compileevaluation.How to test
uv run --extra dev ruff check src/ tests/(Passed as shown in Validation output)uv run pytest tests/unit/test_registry_client.py(Passed as shown in Validation output)apm compileon a dummy package file to assert that dry run instantiation invokes without exceptions.